iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Engineering

Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability系列 第 21 篇

Day 15(上)|Availability:不是把 200 數一數

  • 分享至 

  • xImage
  •  

GitHub:darkstar1227/learning-sre-for-ai-era

結論先說:availability 要以有效使用者請求的結果計算;只用 HTTP 2xx 當 good event,會把錯誤內容和空結果洗成健康。

這個立場其實跟 Day 1 那句「HTTP 200 只能代表技術請求成功,不代表使用者任務成功」是同一件事,只是站的位置不一樣。Day 1 從單一請求的角度說「不要被 200 騙」;Day 15 要把這句話搬到 SLI/SLO 層級,變成一個可以被 alert、被 dashboard、被值班決策使用的量測系統——因為單靠工程師「知道不能只看 200」是不夠的,凌晨三點被 page 起來的人不會逐行讀程式碼判斷這次故障算不算數,他只會看 dashboard;如果 dashboard 本身就是用 200 當作 good 的定義畫出來的,再有經驗的工程師也只能看著一條綠線發呆。這篇要做的,是把「不要被 200 騙」這句提醒,變成一套連值班新人都能照著判斷的系統。

① request-based 與 time-based 問的是不同問題

request-based availability 看「多少請求成功」;time-based availability 看「多少時間服務可用」。互動式 API 通常先用 request-based,因為它能區分午夜沒有流量和午夜全部失敗。批次工作或持續串流則可能需要以時間窗觀察。

availability = good valid requests / all valid requests

good 不等於 2xx。對 /ask 而言,受 contract 保護的 insufficient_context 可能是好結果;HTTP 200 卻少了必要欄位則不是。4xx 是否算分母,取決於它是不是有效使用者操作:錯誤 JSON 通常不算,已驗證身分卻被系統錯誤拒絕則不能藏起來。

為什麼兩種算法不能互相取代

這不是選哪一種比較「精確」的問題。兩種算法問的是完全不同的失敗模式,資料來源也不一樣:

request-based availability
  問:這段期間送出的請求,有多少拿到好結果?
  資料來源:逐筆請求的結果分類
  適合:使用者主動觸發的互動式 API(/ask、/search、/checkout)
  盲點:流量趨近於零時,比率失去統計意義——0/0 或 1/1 都不能代表系統健康

time-based availability
  問:這段期間裡,有多少時間服務被判定為可用?
  資料來源:固定間隔的 synthetic probe 或心跳訊號
  適合:背景 pipeline、batch job、沒有穩定使用者流量的服務
  盲點:「這一分鐘算不算可用」本身是一組規則——探測頻率、逾時定義、
         連續失敗幾次才算「這段時間不可用」,每一個都是可能被無意識放寬的旋鈕

常見誤解是以為這兩種算法只是同一件事的兩種算法,挑一個順手的來用即可。實際上它們可能給出互相矛盾的結論:凌晨流量掉到只剩兩筆請求,其中一筆失敗,request-based availability 瞬間掉到 50%;同一分鐘卻可能因為 synthetic probe 剛好探測成功,被 time-based 算法記為「可用」。兩個數字都「對」,只是在回答不同問題。混用會讓 upstream 的 request-based 曲線跟 downstream 的 time-based 告警彼此矛盾,值班的人會先懷疑資料管線壞了,而不是懷疑系統真的出事。

/ask 這種互動式服務,多數團隊會選 request-based,理由不是「比較準」,而是它天生能區分「凌晨沒人用」跟「凌晨全部失敗」——這兩件事對 time-based 算法而言,如果沒有另外設計 synthetic probe,看起來完全一樣。

「有效請求」要在請求一進來就開始判定

分母的第一道關卡不是 classifier,是進站前的 request validation。一個常見誤解,是以為「valid request」等於「HTTP 層級的請求格式正確」,把這個判斷留給 framework 內建的 422 處理。實際上,一個請求算不算「有效」,是產品契約問題,不是 schema 驗證問題:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()


@app.middleware("http")
async def classify_request_validity(request: Request, call_next):
    if request.url.path == "/ask" and request.method == "POST":
        try:
            body = await request.json()
        except ValueError:
            return JSONResponse(
                status_code=400,
                content={"error": "malformed_json"},
            )
        if "question" not in body or not body["question"].strip():
            return JSONResponse(
                status_code=422,
                content={"error": "missing_question"},
            )
    response = await call_next(request)
    return response

這段 middleware 做的事,是把「這個請求連被服務都不夠格」與「這個請求夠格被服務、但服務的結果不好」分成兩個不同的判斷點。前者在請求真正進到業務邏輯之前就被擋下,永遠不進 availability 的分母;後者才交給第④節的 classify_availability() 決定是 good 還是 bad。兩個判斷點混在一起,最常見的後果是團隊把所有 4xx 一律當成「客戶端的錯,不算我們的」,卻忽略了已通過身分驗證、格式完全正確,卻被系統錯誤拒絕的請求,也可能以 4xx 的外觀出現——這正是第①節開頭那句「4xx 是否算分母,取決於它是不是有效使用者操作」在程式碼層級的樣子。

實例:Netflix 如何定義真正的 availability

Netflix 在雲端遷移後面臨一個根本問題:如何衡量「我們的服務可靠嗎?」傳統監控答案是「監測各個微服務的 uptime」——API 還活著、資料庫還活著、快取還活著。但 Netflix 發現,即使一個內部服務失敗,透過 fallback 與 graceful degradation,使用者可能完全感受不到。

Netflix 改變衡量角度,以「Playback Starts per Second (SPS)」——使用者按下播放鍵後成功開始播放的比例——作為唯一真實的 SLI。這個指標不問基礎設施是否健康,只問「使用者能否完成他們要做的事」。Netflix 用 SPS 驅動整個組織的可靠性決策:一個故障是否值得 page 值班,完全取決於它是否影響 SPS。為了驗證這個架構真的可靠,Netflix 開發了 Chaos Monkey 等工具,定期在生產環境注入故障——在「紙牌屋」首播的最高流量時段,Netflix 故意執行 Chaos Monkey 殺死隨機 instance 進行可用性測試,系統自癒無使用者影響。

這裡有兩個重點:選錯衡量對象,守護的就會跟著錯;要信任一個 SLI,還必須用故障注入驗證它。

實例:Slack 用一個數字說清楚「壞到什麼程度」

2021 年 1 月 4 日,美國假期後第一個工作日早上,Slack 發生近 5 小時的大規模中斷,登入、收發訊息、通話全數受影響。起因並不複雜:假期期間流量低迷,使用者端的快取全部過期;開工當天早上流量瞬間回衝,AWS Transit Gateway(TGW)沒能即時擴容,開始丟包、延遲上升。壞消息接著連環爆:web tier 的 autoscaling 系統先誤判 CPU 下降而縮容,緊接著又在 7:01 到 7:15 短短 14 分鐘內暴力嘗試新增 1,200 台伺服器,結果把負責配置新機器的 provision-service 也一併打爆——撞上 Linux 的 open files 上限與 AWS 的資源配額。

Slack 官方事後報告用一個數字說明這次中斷有多嚴重:平常訊息送達成功率高於 99.999%,但早上 6:57(PST),這個數字掉到 99%。差距看起來只有幾個百分點,換算成使用者體感,卻是每 100 則訊息就有 1 則送不出去——對一個以「訊息即時送達」為核心承諾的產品而言,這已經是重大故障。這正是 request-based availability 該問的問題:不是「Transit Gateway 是否 up」或「web tier CPU 是否正常」,而是「多少次使用者的送出動作真的成功了」。把 99.999% 和 99% 放在同一句話裡比較,比任何一張儀表板截圖都更有說服力,這也是為什麼 SLI 要用具體比率表示,而不是「正常/異常」這種二分燈號。

更值得注意的是,Slack 自己的監控儀表板與告警服務架設在另一個 VPC,而那個 VPC 同樣得經過已經壅塞的 TGW 才能連到後端資料庫——工程師在最需要看清楚狀況的時候,手上的診斷工具本身也在掉線。9:15 恢復到「degraded, not down」,直到 AWS 工程師手動擴充 TGW 容量,10:40 才完全恢復。Slack Engineering:Slack's Outage on January 4th 2021

實例:HTTP Status Code 的騙局

若把「非 5xx = 成功」當作 SLI 規則,會陷入幾個常見的陷阱。舉例:

  • 429 Rate Limit:即使客戶已耗盡文件化的 quota,服務回傳 429 可能代表你的 availability 下降(顧客無法繼續工作),而非「正常拒絕」。
  • 404 Not Found:對不存在的資源回傳 404 是正確的。但如果新搬家的文檔被誤刪,404 突然大幅增加,這不是「好結果」。
  • 401 Unauthorized:認證失敗可能來自客戶端(過期 token),也可能來自服務端(auth provider 故障)。混為一談會隱藏真實的 availability 問題。

429 這一條特別容易被半吊子處理:多數團隊要嘛把所有 429 都算進 bad(結果被自己的方案設計懲罰),要嘛把所有 429 都排除在分母外(結果真正因為系統故障而誤發限流的請求也被藏起來)。兩種做法背後其實藏著同一個問題:429 本身這個 status code,混雜了兩種完全不同的成因,卻共用同一個外觀。

429(客戶已知超額)
+
429(限流服務本身誤判或故障)

這兩者需要不同的 response_status 值才分得開,而不能都塞進同一個籠統的 "rate_limited":

def classify_rate_limit(event: dict) -> str:
    if event["quota_exceeded_by_client"]:
        return "quota_exceeded"          # 依方案設計運作,通常排除或算 good
    return "rate_limiter_misfire"        # 限流邏輯本身可能誤判,通常算 bad

分開之後,PromQL 也能各自獨立觀察:

# 客戶正常超額——這條線正常情況下會隨業務量緩慢起伏
sum(increase(ask_sli_events_total{response_status="quota_exceeded"}[1h]))

# 限流邏輯自己誤判——這條線理論上應該接近於零,一旦墊高就值得調查
sum(increase(ask_sli_events_total{response_status="rate_limiter_misfire"}[1h]))

Google SRE 建議把 SLI 寫成 good events / valid events,而不是從監控系統中挑一個順手的欄位。這個公式看起來普通,難的是先定義兩個集合。正確做法是先明確定義:

  1. 哪些請求屬於「有效請求」(分母)
  2. 「有效請求」中哪些才算「好結果」(分子)
  3. 用低基數 label 記錄因果資訊(如 sli_eligible=true/false, sli_result=success/failure)

而非依賴 HTTP status code 的預設分類。這也讓 SLO、error budget 與告警都能共用同一組輸入,而不是每張 dashboard 各算各的。Google SRE:The Art of SLOs

實例:一個空字串參數,讓「刪除待刪除的」變成「刪除全部」

2026 年 2 月 20 日 17:48 UTC,Cloudflare 內部一支自動化任務,原本的工作是定期清理已被客戶標記刪除的 BYOIP(Bring Your Own IP,客戶自帶的 IP 位址段)前綴。這支任務呼叫 Addressing API 時寫了 /v1/prefixes?pending_delete,卻沒有帶值。API 把這個空字串參數解讀成「這個篩選條件不用管,抓全部」,而不是任務原本想要的「篩選出待刪除的」。結果,這支自動化任務開始有系統地刪除所有客戶的 BYOIP 前綴與其相依物件,不分待刪除還是正常使用中。

這起事故最值得放進本節的地方,不是它的規模(最終約 1,100 個前綴、占總量 25% 被撤銷),而是它整條故障鏈裡,沒有一次 API 呼叫是「格式不合法」的:

自動化任務呼叫
  GET /v1/prefixes?pending_delete
      ↓
query 參數存在,但值是空字串
      ↓
API 沒有區分「這個參數沒被設定」與「這個參數被設定成空字串」
      ↓
兩者都被當成「不篩選,回傳全部」
      ↓
每一筆刪除呼叫,各自都收到 200

對照本文第①節的核心論點——「valid request 是產品契約問題,不是 schema 驗證問題」——這裡的每一次刪除呼叫,HTTP 層級完全合法,連本節開頭的 middleware 範例都攔不下來,因為請求本身沒有缺欄位、沒有型別錯誤。真正壞掉的地方,是「一個空值該被解讀成什麼」這個語意判斷,從一開始就沒有被明確定義成「拒絕」而不是「當成萬用字元」:

def build_pending_delete_query(pending_delete: str | None) -> str:
    if not pending_delete:
        raise ValueError(
            "pending_delete must be an explicit non-empty value; "
            "refusing to silently match all prefixes"
        )
    return f"/v1/prefixes?pending_delete={pending_delete}"

這幾行程式碼要表達的立場,跟本文開頭的 request validation middleware完全一致:一個參數要嘛被明確賦值,要嘛整個請求被拒絕,中間不該有「空字串等於全部」這種隱性預設值。18:13 UTC 才有工程師從 one.one.one.one 網站觀察到異常,比自動化動手(17:48 UTC)晚了 25 分鐘;18:46 UTC 才找到失控的子程序並手動終止,直到 23:03 UTC 全部前綴恢復,總計 6 小時 7 分鐘。Cloudflare 事後報告承認:程式碼審查只聚焦在「面對客戶的 API 呼叫路徑」,沒涵蓋「背景自動化任務在無使用者輸入下自行變更」這類場景。Cloudflare:Cloudflare outage on February 20, 2026

這正是本文容易被忽略的一個延伸:「有效請求」的紀律不只用在系統的對外邊界,也要用在系統內部彼此呼叫的地方。使用者永遠不會直接呼叫這支清理任務,但它判斷錯誤,照樣能讓四分之一的客戶連不上服務——這也是為什麼第⑦節談的「依賴」,不能只想成模型 provider 或資料庫這種顯眼的外部依賴,內部自動化本身也是一種依賴。

② health check 不能取代 SLI

/health 能回答 process 還活著,/ready 能回答是否可接流量。它們不保證 retrieval、provider 與 response contract 對使用者可用。把 health check 全綠當 availability,只是在量一個容易好看的問題。

/health 與 /ready 各自服務誰的決策,而且都不是使用者的問題

Kubernetes 把探測分成兩支端點,是因為它們服務兩個完全不同的自動化決策:

livenessProbe(常見對應 /health)
  問:process 有沒有卡死?
  失敗時的動作:重啟這個 container
  典型實作:回傳 200 就好,甚至不碰任何依賴

readinessProbe(常見對應 /ready)
  問:這個 instance 現在該不該接新流量?
  失敗時的動作:把這個 instance 從 Service 的 endpoint 列表移除,但不重啟
  典型實作:檢查資料庫連線池、快取連線、啟動時載入的設定是否就緒

Day 2 的 Lab 裡,/health 與 /ready 分別對應「這個 process 該不該被殺掉重開」與「這個 instance 現在該不該分到流量」——兩者都是給 Kubernetes 或 load balancer 看的訊號,不是給使用者看的。這正是常見誤解的起點:把 readiness 探測的綠燈,直接當成「使用者現在可以正常使用這個服務」的證據。readiness 探測通常只檢查該 instance 自己能不能連上直接依賴(連線池是否有可用連線、必要設定是否已載入),它不會、也不應該跑一次完整的 /ask workflow 去確認 retrieval、模型 provider 與 response contract 全部正常——那樣的探測成本太高,也會把 synthetic 流量和真實流量混在一起,污染真正的 SLI。

換句話說,readiness 探測與 availability SLI 的差距不是「做得不夠仔細」,而是設計目的完全不同:前者是給編排系統做「路不路由到這台機器」的二元決策,後者是給人做「使用者能不能完成任務」的量化判斷。把兩者疊在同一張儀表板上比較,是把兩個不同座標系的數字硬放在一起。

落到 Day 2 Lab 的實作,兩支端點大致長這樣:

from fastapi import FastAPI, HTTPException

app = FastAPI()


@app.get("/health")
async def liveness() -> dict:
    # 不碰任何依賴,process 還在跑就回 200
    return {"status": "alive"}


@app.get("/ready")
async def readiness(request: Request) -> dict:
    checks = {
        "database": await _check_db_pool(request.app.state.db_pool),
        "cache": await _check_redis(request.app.state.redis),
    }
    if not all(checks.values()):
        raise HTTPException(status_code=503, detail=checks)
    return {"status": "ready", "checks": checks}

對應的 Kubernetes 設定,會分別指到這兩支端點,並給出完全不同的容錯策略:

livenessProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 10
  failureThreshold: 3   # 連續 3 次失敗才重啟,避免瞬間抖動誤殺 container

readinessProbe:
  httpGet:
    path: /ready
    port: 8000
  periodSeconds: 5
  failureThreshold: 1   # 一次沒就緒就先移出流量,成本比 liveness 低

readiness 的 failureThreshold 通常設得比 liveness 低,因為移出流量池是低成本、可逆的動作;重啟 container 則有明確代價(連線需要重建、in-flight request 可能被中斷),所以要更保守地多等幾次確認才動手。這個容錯不對稱,本身就是「兩支探測服務不同決策」最具體的證據——如果它們真的是同一件事,容錯策略沒有理由不一樣。

常見誤解:既然 readiness 探測不夠,那就讓它跑一次完整 workflow

團隊發現「readiness 探測綠燈不代表使用者能用」之後,一個直覺但危險的反應,是把 /ready 改寫成真的去跑一次縮小版的 /ask workflow——呼叫 retrieval、打一次 model provider、驗證 response contract,想藉此讓 readiness 探測更貼近真實的使用者體驗:

@app.get("/ready")
async def deep_readiness(request: Request) -> dict:
    # 反面教材:不要這樣寫
    probe_result = await run_ask_workflow(
        AskRequest(question="ping"), request.app.state
    )
    if not validate_response_contract(probe_result):
        raise HTTPException(status_code=503, detail="workflow_probe_failed")
    return {"status": "ready"}

這樣做會在兩個地方反過來傷害 availability。第一,readiness 探測通常每 5 秒跑一次、每個 instance 各跑一次;如果每次都真的打一次 model provider,探測流量會跟真實使用者流量搶同一份 rate limit 或 GPU 額度,等於監控系統自己在製造負載。第二,更危險:model provider 只是短暫變慢(不是掛掉),這支 deep readiness probe 也會因逾時把所有 instance 同時判「未就緒」、全部移出 endpoint 列表——即使某些 instance 剛好走 cache hit、根本不需要呼叫 provider,也會被一併拖下水,把「provider 有點慢」放大成「全部 instance 一起不健康」。

這正是本節反覆強調的分工原則:readiness 該回答的問題,是「這個 instance 有沒有連上它需要的直接依賴」,不是「這個 instance 現在能不能完美地服務任何一種使用者請求」。後者是 availability SLI 的責任,不該被塞進一個以「秒」為週期、被編排系統無條件信任的探測端點裡。

容易被忘記的第三支探測:startupProbe

Kubernetes 其實還有第三支探測,在只談 liveness 與 readiness 時很容易被忽略:startupProbe。它要回答的問題,跟前兩者都不一樣——不是「process 有沒有卡死」,也不是「現在該不該接流量」,而是「這個 container 第一次啟動,是不是還在合理的暖機時間內」:

startupProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 5
  failureThreshold: 30   # 最多容忍 5 秒 × 30 次 = 150 秒的啟動時間

startupProbe 存在的理由,是解決 liveness probe 一個天生的兩難:如果 AI workflow 服務啟動時要先載入 embedding model、建立 retrieval index 的連線池,這個過程可能需要一兩分鐘。若這段暖機時間完全交給 liveness probe 判斷,failureThreshold 要嘛設得很寬鬆(幾分鐘都不重啟),代價是之後真的卡死時也要等一樣久才被發現;要嘛設得很嚴格,代價是服務每次冷啟動都會被 liveness probe 誤判成「卡死」而被反覆重啟,永遠開不了機。startupProbe 把「啟動期」跟「穩定運行期」分成兩套獨立的容錯設定:startupProbe 通過之前,liveness 與 readiness 探測完全不會被執行;一旦通過,才交棒給前兩支探測用各自的節奏繼續監看。這跟第①節「不同集合要用不同的判斷邏輯」是同一種思路,只是這裡分的不是 valid/invalid request,而是「啟動中」與「已就緒運行」這兩個服務生命週期階段。

實例:Meta 2021 年 10 月事件

2021 年 10 月 4 日,Meta(Facebook)在例行維運中執行一條命令,意圖檢查全球骨幹網容量。這個命令不慎斷開了所有資料中心間的連線。表面上看,Meta 有多個資料中心冗餘、多套 DNS 伺服器、分散式架構。Health check 機制應該早就發現問題才對。但實際情況遠複雜:

  • DNS 伺服器在物理上仍在運作,但無法與資料中心通訊
  • 自動化系統偵測到骨幹網「不健康」,於是自動禁用 BGP 廣告
  • 結果是 DNS 伺服器對外宣告「我不可達」
  • 內部診斷工具(VPN、遠端存取系統)本身也依賴這個故障的骨幹網
  • 工程師無法遠端登入系統進行修復
  • 最後必須派人親赴資料中心現場手動操作,才能恢復

Meta 的事後說明指出,骨幹中斷後,DNS 對外撤回 BGP 宣告;同一場故障也破壞了平常用來調查與修復的內部工具,因此必須派工程師到資料中心處理。事故持續約六到七小時,公開報導估計影響超過一千萬名使用者。Meta Engineering:More details about the October 4 outage

教訓很直接:告訴你系統壞了的路徑,不能完全依賴那個正壞掉的系統。這正好呼應前一段的 readiness 探測設計原則——如果一個 instance 的 readiness 探測本身要透過同一條正在故障的骨幹網才能回報結果,這個探測本身就已經失效,卻可能還在回傳「看起來正常」的假訊號。

Health check 全綠 ≠ 使用者能存取服務。Availability SLI 必須反映「在真實故障下系統能否恢復」,而不只是「某個瞬間的健康狀態」。

③ 先把使用者旅程切出來

/ask 是一條路徑,不是一個單一元件。使用者按下送出後,可能依序經過認證、API、retrieval、模型供應商、parser、validator 與回應串流。availability SLI 要量的是這條路徑的結果,不是其中任何一段「看起來還活著」。

把一條使用者旅程拆成好幾段元件,這件事本身有一個常被忽略的數學後果:即使每一段元件各自都有 99.9% 的可用性,只要旅程串連了五段,整條路徑的理論上限也只有大約 99.5%(0.999^5 ≈ 0.995)——這還是假設各段失敗互相獨立、沒有級聯效應的樂觀估計。監控每一段元件各自的健康度,永遠無法直接告訴你整條路徑的可用性;只有把整條旅程當成一個單位去量測,才量得到使用者真正經歷的數字。這也是為什麼 Day 2 建置的 Prometheus/Loki/Tempo 三件套裡,trace 要負責把一次 /ask request 經過的每一段串起來——沒有這條串連,availability 只能用猜的。

Browser
  ↓
POST /ask
  ↓
request validation ── invalid payload → 不屬於有效事件
  ↓
authentication ────── service-side rejection → 可能是 bad event
  ↓
retrieval
  ↓
model / tool calls
  ↓
response validation
  ↓
completed / insufficient_context / requires_human_review

這張圖不是要把每一個失敗都算成 availability 失敗。它的用途是逼團隊逐段回答:這個結果是否在使用者要求的服務範圍內?若在範圍內,使用者是否拿到了 contract 所承諾的結果?

常見誤解:以為某一段綠燈,代表使用者旅程綠燈

團隊很容易把 dashboard 上每一段的健康度都看過一遍,覺得「retrieval 正常、model provider 正常、validator 正常,所以使用者應該沒問題」。這個推論在傳統系統裡通常成立,因為每一段的「正常」定義相對明確(連線建立、查詢回傳、無 exception)。在 AI workflow 裡,這個推論會在兩個地方悄悄崩壞:

  1. 每一段各自都「正常結束」,但銜接處丟失了資訊。 retrieval 回傳的文件正確,模型也正常生成了回答,但 prompt 組裝時漏放了某個關鍵文件的片段——這不會讓任何一段回報失敗,因為每一段都完成了它被要求做的事,只是彼此傳遞的內容已經不對。
  2. 某一段的「失敗」被下一段吸收成看似正常的結果。 模型呼叫逾時,但上層邏輯捕捉了例外,回傳一個預設的萬用回答。從 availability 的角度,這個 request 完成了、格式正確、沒有 exception 冒出來——這正是本文一路強調的:技術上完成,不代表 contract 被滿足。

這兩種情況都不會出現在單一元件的健康 dashboard 上。要看到它們,唯一辦法是把整條旅程的輸入與輸出串起來檢查,而不是逐段確認「這段有沒有報錯」。

不同結果,不能用同一把尺

以下表格是 Day 14 的 SLO spec 在 /ask 上可能長出的第一版分類。它不是通用答案;產品的帳號、方案、資料權限與風險等級不同,分類也會不同。

事件 是否 valid 是否 good 為什麼
正常回答,符合 response contract 是 是 使用者取得服務承諾的回應
找不到可信來源,回 insufficient_context 是 通常是 誠實拒答可能正是產品契約
需要人工覆核而回明確狀態 是 視契約而定 若流程承諾能排入覆核,未必是失敗
API 逾時 是 否 有效請求沒有在約定時間得到結果
模型 5xx,且沒有可用降級路徑 是 否 使用者無法完成當次任務
回 HTTP 200,但缺 answer_status 是 否 transport 成功,response contract 失敗
缺少必填欄位的 request 否 不適用 不是服務承諾的有效操作
已驗證使用者被 auth service 誤拒 是 否 產品端故障不該藏在 401 後面
使用者超過明文告知的方案額度 需決策 需決策 這是產品承諾問題,不是 HTTP 類別問題

表格裡最不舒服的欄位是「需決策」。它不代表規格沒寫完就可以略過,而是提醒你找產品 owner、support 與安全負責人一起定義。SRE 能提供量測方式,不能單方面宣布「限流中的客戶不算使用者」。

以「使用者超過明文告知的方案額度」這一列為例,實務上至少有三種站得住腳的答案,各自對應不同的產品立場:

立場一:超額請求不是服務失敗,是方案設計本身在運作
  → valid=是、good=是;額度限制記進獨立的 quota metric,不進 availability

立場二:超額請求代表使用者的任務沒有完成
  → valid=是、good=否;availability 要反映「使用者拿不到答案」這個事實

立場三:超額請求不是「使用者的操作」,是系統依方案設定執行的結果
  → valid=否;完全排除在 availability 分母外,理由與惡意流量、爬蟲請求相同

三種立場都邏輯自洽,差別在於公司要不要把「已知的方案限制」算進「服務有沒有達成承諾」。選哪一種,會直接影響 error budget 被誰的行為消耗——如果選立場二,一次促銷活動帶來的爆量超額請求,可能瞬間吃光當月的 error budget,即使系統本身一切正常運作。這正是為什麼這一格不能由 SRE 自己拍板:它牽涉商業條款怎麼寫、客服怎麼回覆客訴,遠超出「這個 request 技術上有沒有成功」的範疇。

AI workflow 特有的邊界

Day 7 已經把 technical success、workflow success 與 task success 拆開。availability 指標也需要守住這個邊界。

假設模型回覆了格式正確的 JSON,validator 也通過。這足以讓一個 technical availability SLI 計為 good event;它仍不證明答案是真的、引用正確,或使用者完成工作。那些要交給 quality、safety 與 task-success 的 SLI。把所有事情硬塞到 availability,最後只會得到一個誰也解不釋的總分。

Availability SLI
  問:有效請求是否在約定時間得到可用的服務狀態?

Quality SLI
  問:回答是否符合已定義的正確性或 groundedness 標準?

Safety SLI
  問:系統是否依政策拒絕、升級或限制高風險操作?

Task-success SLI
  問:使用者是否完成原本要做的事?

同一筆 request 可以 technical availability 為 good、quality 為 unknown。這不是缺陷,而是誠實的資料模型。尚未評估的品質,不要因為 API 回 200 就自動補成 pass。

四條 SLI 的資料來源也刻意分開,不要幻想能用同一套 event schema 全部涵蓋。Availability 的 good/bad 由第④節的 classify_availability() 即時判定,成本極低,可以掛在每一筆請求上;Quality 通常需要另外跑一組 LLM-as-judge 或人工抽樣評分,成本高、有延遲,往往是離線、抽樣或非同步進行;Safety 的判斷常常要接規則引擎或分類模型,且必須是同步、擋在回應送出之前;Task-success 則多半要靠使用者的下一步行為(有沒有繼續追問、有沒有放棄、有沒有回頭改用別的方式)才能間接推得,時間尺度可能拉長到數小時或數天之後才確定。把這四條線的資料管線混在一起做,例如硬要求 quality 評分跟 availability 判定用同一個同步呼叫完成,只會讓 availability 也被迫背上 quality 評分的延遲與成本,違反第①節「availability 要能低成本、高頻率地即時計算」的前提。

實務上,這四條線的資料管線大致長這樣:

使用者請求
  ↓
route handler 同步完成 availability 判定(第④節 classify_availability)
  ↓ ── 立即回應使用者 ──
  │
  └─→ 把這筆 request/response 寫進佇列
        ↓(非同步、離峰批次或抽樣)
      LLM-as-judge 或人工抽樣評分 → quality SLI
        ↓(數小時到數天後)
      使用者的下一步行為(追問/放棄/改用其他方式)→ task-success SLI

Safety 是這張圖裡唯一的例外:它必須是同步、擋在回應送出之前,不能等離峰批次才判定——一個高風險操作若靠事後抽樣才發現,使用者早就已經受到影響。這也是為什麼四條 SLI 不能共用同一套 pipeline:availability 要快、safety 要同步且不能被繞過、quality 要準但可以晚一點、task-success 本來就急不來。把「快」的需求跟「準但慢」的需求綁在同一次呼叫裡,通常是兩敗俱傷——availability 被拖慢,quality 評分為了趕上即時性又只能做得很粗糙。

實例:RAG 系統裡,八種讓答案出錯的路徑,沒有一種會讓 API 報錯

如果只看 availability 字面上的定義,「檢索器有回傳文件、模型有生成回答、HTTP 200」看起來已經是完整的成功旅程。業界對 RAG(retrieval-augmented generation)系統的觀察至少整理出八種常見的語義失敗模式,每一種都能在完全不觸發任何 error 的情況下讓答案出錯:關鍵答案剛好落在檢索到的第 14 個 chunk(總共 20 個),但 transformer 的注意力機制對中段內容的權重偏低,模型改用了排在前面、其實不相關的 chunk;代名詞指涉的名詞出現在沒被檢索到的前一個 chunk 裡,模型只能瞎猜代名詞指的是誰;文件明明寫著某個數字,模型卻因為訓練時記住的內部知識,回答了另一個數字;三份不同年份的政策文件同時被檢索出來,模型把版本混在一起、甚至取平均值;需要跨兩個 chunk 才能推導出的邏輯結論,模型只總結了其中一個 chunk 就下結論;Markdown 表格在文字擷取過程中欄位對不齊,模型讀到的是錯位後的數字;文件裡根本找不到答案時,模型仍然禮貌地生成一個聽起來合理的回覆。

這八種模式的共通點,正是 availability SLI 的天生盲區:檢索器沒有報錯(它確實找到了文件,只是不是對的文件或不是完整的文件)、模型沒有報錯(它確實生成了語法通順的回答)、response validator 如果只檢查 JSON schema 是否合法,也不會報錯。要抓到這些情況,必須依賴獨立的 quality 或 groundedness 評估——這正是為什麼本文一路把 quality SLI 從 availability SLI 拆開來看,而不是假設「檢索有結果、模型有輸出」就等於「這次請求是好的」。DEV Community:Your RAG Retrieved the Right Document, So Why Was the Answer Wrong?

實例:Anthropic 自己也踩過「技術完成、回答悄悄壞掉」

這不是假設情境。2025 年 8 月到 9 月間,Anthropic 同時遇上三個彼此獨立的基礎設施 bug,讓 Claude 的部分回應被悄悄弄壞,而 API 層級的請求看起來完全正常——沒有 5xx、沒有 timeout,stop_reason 照樣是正常結束。三個 bug 分別是:

  1. routing 錯誤:短 context 的請求被誤導向設定給 1M token context window 的伺服器。8 月 5 日只影響 0.8% 的 Sonnet 4 請求,到 8 月 31 日已惡化到 16%,約三成 Claude Code 使用者至少中過一次;由於採 sticky routing,一旦某個對話被導向錯誤的伺服器,後續同一串對話的請求也會持續走同一條錯路。
  2. 輸出亂碼:一台 TPU 伺服器設定錯誤,讓 token 生成過程出錯。使用者可能在一段正常的英文回答中間,忽然看到一串泰文字元「สวัสดี」,或是毫無來由的語法錯誤。
  3. 編譯器誤算:底層 XLA:TPU 編譯器的一個潛伏 bug,在特定 batch size 與模型組合下,讓近似 top-k 運算「完全算錯,但只在某些情況發生」;而且行為會隨著前後跑過的其他運算、除錯工具是否開啟而改變,難以穩定重現。

Anthropic 在事後報告裡坦承,內部的自動評測並沒有抓到使用者回報的品質下降,部分原因是「Claude 經常能從單次錯誤中恢復得很好」——一段回答裡混進一次錯誤,整體評測分數仍可能被拉高、被稀釋掉;同時,內部的隱私與安全控管,讓工程師無法直接翻閱使用者的實際對話內容做根因分析,只能仰賴使用者主動回報才拼湊出問題全貌。Anthropic:A postmortem of three recent issues

對照本文的分類方式:這三個 bug 造成的請求,在 contract_valid 的意義上其實都是失敗——回應偏離了「這應該是一段連貫、正確語言的完成內容」這個契約。但如果 SLI 的定義只看 HTTP status 或「有沒有跑完」,這些請求會被算成 100% 可用。技術上的「完成」和「契約被滿足」是兩件事,availability SLI 只保證前者;這正是本文一路強調要把 quality、task-success 拆成獨立 SLI 的原因,不是把它們硬塞進同一個 availability 分數,讓稀釋掉的錯誤永遠躲在總分背後。

④ 用事件分類取代猜 status code

一個可維護的做法,是讓 application 在知道完整結果後,輸出明確的 SLI event。Prometheus 只負責累計;「這次算不算 good」由程式碼中的 service contract 決定,並以測試保護。

先定義低基數 labels

availability counter 的 label 必須可以安全聚合。request_id、user_id、完整 prompt、文件 ID 都不能放進 Prometheus label,否則 time series 數量會一路膨脹。它們應留在 logs 或 traces。

from prometheus_client import Counter

ask_sli_events_total = Counter(
    "ask_sli_events_total",
    "Classified /ask events for availability SLI",
    labelnames=(
        "sli_eligible",
        "sli_result",
        "response_status",
    ),
)

這裡三個 label 的基數都可控制:

label 允許值範例 不該放什麼
sli_eligible true、false 每個 request 的判定理由全文
sli_result good、bad、excluded exception message
response_status completed、timeout、invalid_contract 任意模型輸出

若需要知道哪一個模型或 prompt version 出問題,可以在 trace 或 structured log 裡記 model_route、prompt_version、retrieval_index_version。這些欄位通常會變動,未必適合放在每一條 metrics time series。

把 classifier 寫得比 if/else 更清楚

以下範例把有效性、結果與理由分開。這樣測試失敗時不只知道 False,也知道規則怎麼判斷。

from dataclasses import dataclass
from enum import StrEnum


class SliResult(StrEnum):
    GOOD = "good"
    BAD = "bad"
    EXCLUDED = "excluded"


@dataclass(frozen=True)
class ClassifiedEvent:
    eligible: bool
    result: SliResult
    reason: str


GOOD_RESPONSE_STATUSES = {
    "completed",
    "insufficient_context",
    "requires_human_review",
}


def classify_availability(event: dict) -> ClassifiedEvent:
    if not event["valid_request"]:
        return ClassifiedEvent(False, SliResult.EXCLUDED, "invalid_request")

    if event["latency_ms"] > 10_000:
        return ClassifiedEvent(True, SliResult.BAD, "deadline_exceeded")

    if not event["contract_valid"]:
        return ClassifiedEvent(True, SliResult.BAD, "invalid_response_contract")

    if event["response_status"] in GOOD_RESPONSE_STATUSES:
        return ClassifiedEvent(True, SliResult.GOOD, "contract_satisfied")

    return ClassifiedEvent(True, SliResult.BAD, "workflow_failed")

這段程式的重點不是 StrEnum。真正重要的是規則順序:無效 request 在最前面排除;逾時與 contract failure 是 valid-but-bad;只有列入服務契約的 terminal status 才計 good。若你的產品把 requires_human_review 視為未完成任務,就把它從 GOOD_RESPONSE_STATUSES 移除,並同步修改 Day 14 的 SLO spec。

規則順序本身也是一份會被讀出來的文件。把「無效 request」放在最前面,等於明確表態:不管後面的延遲或內容再怎麼異常,只要這筆請求一開始就不合法,就完全不進 valid-request 分母,不受後面任何規則影響。把「逾時」放在「contract 是否合法」之前,代表這個服務認為「等太久」本身就是一種失敗,即使最後真的等到了語法正確的回應,也不能抵銷使用者已經等超過約定時間這個事實——如果順序反過來,一筆等了 15 秒但格式正確的回應會被誤判成 good,只因為程式先檢查了 contract,那正好符合的分支比逾時分支先觸發,卻沒實質改變使用者體驗到的等待時間。任何想調整這五條規則順序的人,都應該先問「這個順序在表達哪一種產品立場」,而不是純粹依直覺把測試會不會過當唯一的判斷標準。

常見誤解:以為多切幾種 response_status 就能取代 classifier

團隊有時候會用另一種偷懶方式:把判斷邏輯直接寫進 response_status 的字串值裡,例如把 completed_but_low_confidence、completed_with_fallback、completed_partial 都當成獨立的 status,然後在 Prometheus label 或 dashboard 查詢裡手動列出「哪些字串算 good」。這個做法有兩個實際的維護代價:一是每次新增一種 status,所有查過這條規則的 dashboard、alert、classifier 呼叫端都要同步更新,很容易漏掉一處;二是「這個 status 算不算 good」的判斷邏輯散落在多個地方,沒有單一測試能保護所有呼叫端同時正確。

把判斷邏輯集中在 classify_availability() 這一個函式,好處不是程式碼比較短,而是「good 的定義」永遠只有一個真相來源。response_status 只負責描述「發生了什麼」,sli_result 才負責回答「這算不算好結果」——兩者職責分離之後,改變業務規則只需要改一個函式,並且立刻由既有的 fixture 測試出是否真的符合預期,而不是祈禱自己記得改齊每一處。

在請求完成後才記錄

不要在 request 一進來就把 good counter 加一。那是預測,不是量測。應在 response model 已驗證、deadline 已判定後再累計。

def record_availability_event(event: dict) -> ClassifiedEvent:
    classified = classify_availability(event)

    ask_sli_events_total.labels(
        sli_eligible=str(classified.eligible).lower(),
        sli_result=classified.result.value,
        response_status=event["response_status"],
    ).inc()

    return classified

若 application 在 process crash 前就中斷,這類 event 可能完全沒有被記到。這是 server-side instrumentation 的盲點,不是把 counter 多加一個 label 能解決。之後可以用 ingress、load balancer 或 synthetic probe 的獨立訊號交叉檢查;但不要把兩種資料來源混成同一個分母而不留下來源說明。

這個盲點具體會有多大,取決於 crash 發生的頻率與流量規模。假設一個服務平均每天 crash 三次,每次從程序異常到 container 被 liveness probe 判死、重啟完成,中間大約有兩秒鐘的請求會落在這個盲點裡;如果尖峰時段每秒 50 個請求,一次 crash 大約會漏記 100 筆事件,一天三次就是 300 筆——對照第⑤節的 28 天分母 10,000 筆,這已經接近 3%,不是可以忽略的雜訊。更麻煩的是,這些漏記的事件幾乎必然全部是 bad(畢竟它們發生在服務正在崩潰的那個瞬間),漏記等於系統性地低估了失敗數,讓 availability 看起來比實際更好,而不是隨機誤差。這正是為什麼交叉核對 ingress 或 load balancer 這類獨立於 application 本身的訊號有其必要——它們能看到 application 連記錄自己失敗都來不及的那個空隙。

把三段邏輯串成一個 route handler

前面分別看了 request validation、classifier 與 metric 記錄。實際的 /ask route,大致是把這三段依序串起來:

from fastapi import APIRouter, Depends, HTTPException
from time import monotonic

router = APIRouter()


@router.post("/ask")
async def ask(payload: AskRequest, deps=Depends(get_dependencies)) -> AskResponse:
    started_at = monotonic()

    try:
        raw_result = await run_ask_workflow(payload, deps)
        contract_valid = validate_response_contract(raw_result)
        response_status = raw_result.status
    except WorkflowTimeoutError:
        contract_valid = False
        response_status = "timeout"
        raw_result = None
    except Exception:
        contract_valid = False
        response_status = "unhandled_error"
        raw_result = None

    latency_ms = int((monotonic() - started_at) * 1000)

    event = AvailabilityEvent(
        valid_request=True,   # 已通過前面的 request validation middleware
        latency_ms=latency_ms,
        contract_valid=contract_valid,
        response_status=response_status,
    )
    record_sli_event(event)

    if raw_result is None:
        raise HTTPException(status_code=502, detail=response_status)
    return raw_result

這裡刻意把 record_sli_event(event) 放在 try/except 之後、回傳給使用者之前——不管 workflow 是正常完成、逾時,還是拋出未預期的例外,都要先完成分類與記錄,再決定要回傳什麼給呼叫端。常見的錯誤寫法,是只在 try 區塊成功的分支裡呼叫 record_sli_event(),這樣一來,所有例外路徑(逾時、未預期錯誤)都不會產生任何 SLI event——這不是「這些請求被算成 good」,而是更糟的「這些請求完全沒被記錄」,會讓分母比真實情況更小,availability 比率因此虛假偏高。

⑤ PromQL:先把分子與分母寫出來

假設上面的 counter 已被 Prometheus scrape,先從能直接閱讀的 query 開始。

為什麼每個 query 都包著 increase()

ask_sli_events_total 是 Prometheus 的 Counter 型別,它只會遞增,不會遞減——記的是「從服務啟動以來,這個 label 組合出現過幾次」的累積值,而不是「現在有幾筆」。直接讀 counter 目前的數值沒有意義:它會隨服務跑得越久持續變大,也會在 process 重啟時歸零重新累加。要回答「過去 28 天發生幾次」這種問題,得用 increase() 讓 Prometheus 依時間序列自動算出時間窗內的差值,並處理 counter reset(重啟後從 0 重新累積)造成的斷點——若忽略這件事,直接對原始 counter 值做 sum(),遇到任何一次 pod 重啟,這個時間窗的數字就會無故偏低甚至出現負值的錯覺。這也是為什麼下面每一條 availability query,都是先 increase() 再 sum(),而不是反過來。

有效請求數:分母

sum(
  increase(ask_sli_events_total{sli_eligible="true"}[28d])
)

這裡必須只選 sli_eligible="true"。如果把 excluded 也加進分母,壞 client 送的無效 JSON 越多,SLO 看起來反而越差;相反地,若把 service-side auth failure 標錯為 excluded,SLO 又會假性變好。

good events:分子

sum(
  increase(
    ask_sli_events_total{
      sli_eligible="true",
      sli_result="good"
    }[28d]
  )
)

availability ratio

(
  sum(increase(ask_sli_events_total{sli_eligible="true", sli_result="good"}[28d]))
)
/
(
  sum(increase(ask_sli_events_total{sli_eligible="true"}[28d]))
)

若分母為零,這個 query 可能回空值或產生不適合解讀的結果。低流量服務不能假裝有一份精準的 availability 百分比。Dashboard 應同時顯示 valid event count;資料太少時,顯示「樣本不足」比畫一條 100% 的綠線更誠實。

樣本數不夠時,百分比本身就是雜訊

「分母為零」是最極端、最容易被發現的情況;更常被忽略的是分母雖然不是零,但小到讓百分比失去意義。假設一個內部工具型的 /ask 服務,週末凌晨兩點只有 3 筆有效請求,其中 1 筆因為下游逾時被判 bad,這段 1 小時窗口算出來的 availability 是 2 / 3 ≈ 66.7%——數字看起來很嚇人,實際上只代表三個樣本裡有一個不走運,跟平日尖峰時段真正累積出的 9,940 / 10,000 = 99.4% 完全不是同一個等級的訊號。如果告警規則不分青紅皂白地對任何低於 SLO 門檻的比率開警報,這種低流量時段會不斷製造假警報,值班的人很快就會學會忽略它——而「學會忽略告警」正是最危險的副作用,因為下一次真正的故障,也會被同一套已經被調成忽略模式的直覺放過。

比較穩妥的做法,是替告警規則加上一個最小樣本數門檻,只在分母超過某個閾值時才評估比率是否違反 SLO;分母不足時,寧可讓面板顯示「樣本不足,暫不評估」,也不要硬算出一個統計上不可信的百分比。這也是為什麼第⑤節一路強調 dashboard 要同時秀出 valid event count,而不是只秀一條百分比曲線——沒有分母當背景,看的人無從分辨這條曲線這一刻到底代表「系統穩定地很好」還是「樣本少到隨便晃」。

bad event 依 response_status 拆解

單一的 availability ratio 告訴值班的人「壞了多少」,不會告訴他「壞在哪裡」。要回答後面這個問題,把 sum() 換成 sum by (response_status)(),讓同一組資料依 label 拆開:

sum by (response_status) (
  increase(
    ask_sli_events_total{
      sli_eligible="true",
      sli_result="bad"
    }[1h]
  )
)

這條 query 回傳的不是單一數字,而是一組依 response_status 分組的時序,例如 timeout: 12、invalid_response_contract: 3、workflow_failed: 1。把這三組畫成同一張圖上的堆疊面積圖,比只看一條總和曲線更能回答「這次惡化是哪一種原因主導的」——如果 timeout 那條線突然墊高,該查的是下游 provider 或 retrieval 的延遲;如果是 invalid_response_contract 墊高,該查的通常是最近一次 prompt 或 parser 變更,這正好對應第⑩節的 incident walkthrough。

用小數字驗算一次

假設最近 28 天有下列分類:

valid + good:      9,940
valid + bad:          60
excluded:            400

availability 的分母是 9,940 + 60 = 10,000,不是 10,400。

availability = 9,940 / 10,000 = 0.994 = 99.4%

如果 SLO 是 99.5%,這段期間沒有達標。別把 400 筆 excluded 拿來稀釋分母,也別把 60 筆 bad 說成「只是模型偶爾慢」。每一筆分類都必須能回到規格與證據。

用 recording rule,讓 28 天視窗不用每次現算

increase(...[28d]) 這種長時間窗查詢,在 Grafana 每次刷新、或者 alert rule 每分鐘評估一次時,都要求 Prometheus 重新掃過 28 天份的原始 sample。流量夠大的服務,這個查詢的計算成本會隨時間窗長度線性增加。實務上的做法是用 recording rule 把中間結果先算好、存成新的 time series,之後的查詢與告警都只讀這個已經算好的結果:

groups:
  - name: ask_availability
    interval: 1m
    rules:
      - record: ask:sli_events:increase28d
        expr: |
          sum(increase(ask_sli_events_total{sli_eligible="true"}[28d]))
      - record: ask:sli_good_events:increase28d
        expr: |
          sum(increase(ask_sli_events_total{sli_eligible="true", sli_result="good"}[28d]))
      - record: ask:availability_ratio:28d
        expr: |
          ask:sli_good_events:increase28d / ask:sli_events:increase28d

ask:availability_ratio:28d 之後可以直接被 dashboard 與 alert rule 引用,不必每次都重新展開兩層 sum(increase(...))。這不只是效能優化——它也讓分子、分母的定義被固定在一個地方,避免不同的 dashboard 各自寫出稍微不一樣的 PromQL,卻自以為在算同一個數字。

為什麼 recording rule 的 interval 要設 1 分鐘,而不是 5 分鐘或 1 小時

這裡有個容易被跳過的細節:上面那組 recording rule 的 interval: 1m,看起來只是一個效能參數,其實跟下一節要談的 multi-window burn rate alert 綁在一起。原因是 burn rate alert 最短的評估視窗通常是 5 分鐘,如果 recording rule 每 5 分鐘才更新一次結果,alert 拿到的其實是「5 分鐘前算出來、現在才被讀到」的數字,短視窗告警原本想抓的是「現在正在發生的異常」,卻被 recording rule 自己的更新頻率拖慢了偵測速度。

具體換算:假設 burn rate alert 用的短視窗是 5 分鐘、長視窗是 1 小時,這兩個視窗都是「查詢時往回看多久」,跟 recording rule 多久重算一次是兩件事。如果 recording rule 的 interval 設成跟短視窗一樣的 5 分鐘,最壞情況下告警會延遲將近一個短視窗的長度才真正觸發——因為 recording rule 剛好在異常發生後才完成上一輪計算,下一輪還要再等 5 分鐘。設成 1 分鐘,則延遲上限壓到 1 分鐘內,遠小於短視窗本身的長度,不會變成偵測速度的瓶頸。

這也是為什麼這份 recording rule 特地用兩層:先算出 28 天的 increase,再算比率,而不是一次到位。分開算的好處是,ask:sli_events:increase28d 這種中間結果也可以直接被別的 alert rule(例如流量驟降偵測)重複使用,不用每個關心「28 天內有多少 SLI event」的地方各自重寫一次 sum(increase(...))。

下集預告

有了分子、分母與 PromQL,下篇要回頭處理一個問題:28 天的長 window 跟事故當下的短 window,看到的根本不是同一件事。接著會把 classifier 動手做出來,並直播一場只看 HTTP status 會整個錯過的事故。


這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 14(下)|SLI、SLO、SLA:先量承諾,再談百分比
下一篇
Day 15(下)|Availability:不是把 200 數一數
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 44 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言